import ast
from exception import TypeJsonParseError


class Checker:
    def __init__(self):
        pass

    @staticmethod
    def check_def_type(def_ast):
        # check type:
        # check the reference parent against definition parent
        parent_ref = def_ast.parent_type_ref_symbol
        if parent_ref is None:
            return
        # check parent type params
        Checker.check_ref_applies_def(def_ast.parent_type_ref_symbol, parent_ref.type_tag.ast)

        # check members
        Checker.check_def_members(def_ast)

    # list all members of a non-generic type
    @staticmethod
    def __list_all_non_generic_members(a_def, members=None):
        if members is None:
            members = []
        for m in a_def.def_members:
            members.insert(0, m)
        Checker.list_all_members_of(a_def.parent_type_ref_symbol, members)
        return members

    @staticmethod
    def __get_real_type(type_ref_symbol, type_map, line_col):
        type_symbol = type_ref_symbol.type_tag.ast
        type_tag = type_ref_symbol.type_tag
        if type_tag in type_map:
            return type_map[type_tag]
        else:
            # type_symbol is TypeSymbol
            if type_ref_symbol.type_ref_symbols is None:
                s = ast.TypeRefSymbol(type_symbol.type_name, None, type_ref_symbol.line_col)
                s.type_tag = type_ref_symbol.type_tag
                return s
            else:
                g_type_ref_symbols = type_ref_symbol.type_ref_symbols
                new_g_type_symbols = []
                for g in g_type_ref_symbols:
                    new_g_type_symbols.append(Checker.__get_real_type(g, type_map, line_col))
                new_type_ref_symbol = ast.TypeRefSymbol(type_symbol.type_name, new_g_type_symbols, line_col)
                new_type_ref_symbol.type_tag = type_symbol.type_tag
                return new_type_ref_symbol

    # list all members of a generic type
    @staticmethod
    def __list_all_generic_members(def_ast, g_arr, members=None):
        if members is None:
            members = []
        def_type = def_ast.type_symbol
        type_map = {}
        for i in range(0, len(g_arr)):
            real_type = g_arr[i]
            g_type_tag = def_type.generic_type_symbols[i].type_tag
            type_map[g_type_tag] = real_type
        for member in def_ast.def_members:
            member_type_ref_symbol = member.optional_type_symbol.type_ref_symbol
            new_type_symbol = Checker.__get_real_type(member_type_ref_symbol, type_map, member_type_ref_symbol.line_col)
            new_member = ast.DefMember(member.key,
                                       ast.OptionalTypeSymbol(new_type_symbol, member.optional_type_symbol.optional,
                                                              member.optional_type_symbol.line_col), member.line_col)
            members.append(new_member)

        # check parent
        parent_type_ref_symbol = def_ast.parent_type_ref_symbol
        if parent_type_ref_symbol is None:
            return members
        if parent_type_ref_symbol.type_ref_symbols is None:
            # noinspection PyTypeChecker
            return Checker.list_all_members_of(parent_type_ref_symbol, members)
        else:
            parent_g_arr = []
            for g in parent_type_ref_symbol.type_ref_symbols:
                if g.type_tag in type_map:
                    new_g = type_map[g.type_tag]
                    parent_g_arr.append(new_g)
                else:
                    if g.type_ref_symbols is None:
                        parent_g_arr.append(g)
                    else:
                        new_g = Checker.__get_real_type(g, type_map, g.line_col)
                        parent_g_arr.append(new_g)
            return Checker.__list_all_generic_members(parent_type_ref_symbol.type_tag.ast.a_def, parent_g_arr, members)

    # list all members of a type (generic or non-generic)
    @staticmethod
    def list_all_members_of(type_ref_symbol, members=None):
        if members is None:
            members = []
        if type_ref_symbol.type_ref_symbols is None:
            return Checker.__list_all_non_generic_members(type_ref_symbol.type_tag.ast.a_def, members)
        else:
            g_arr = type_ref_symbol.type_ref_symbols
            def_ast = type_ref_symbol.type_tag.ast.a_def
            return Checker.__list_all_generic_members(def_ast, g_arr, members)

    @staticmethod
    def __check_untyped_value(value):
        if isinstance(value, ast.Object):
            members = value.members
            for mem in members:
                v = mem.value
                if v.type_ref_symbol:
                    Checker.check_value(v.type_ref_symbol, True, v.value)
                else:
                    Checker.check_value(None, False, v.value)
            res_value = {}
            for member in members:
                k = member.key.string
                v = member.value.value.res_value
                res_value[k] = v
            value.res_value = res_value
        elif isinstance(value, ast.Array) or isinstance(value, ast.Set):
            elements = value.elements
            for ele in elements:
                if ele.type_ref_symbol:
                    Checker.check_value(ele.type_ref_symbol, True, ele.value)
                else:
                    Checker.check_value(None, False, ele.value)
            res_value = []
            for elem in value.elements:
                res_value.append(elem.value.res_value)
            value.res_value = res_value
        elif isinstance(value, ast.Number):
            value.res_value = value.num
        elif isinstance(value, ast.String):
            value.res_value = value.string
        elif isinstance(value, ast.Boolean):
            value.res_value = value.boolean
        else:
            raise Exception('bug! unknown value type: ' + str(type(value)))

    @staticmethod
    def check_value(type_ref_symbol, type_specified, value):
        if isinstance(value, ast.Null):
            value.res_value = None
            return
        if type_ref_symbol is None:
            Checker.__check_untyped_value(value)
            return
        if type_ref_symbol.type_tag.ast.built_in:
            name = type_ref_symbol.type_name
            if name == 'Any':
                Checker.__check_untyped_value(value)
            elif name == 'Number':
                if isinstance(value, ast.Number):
                    value.res_value = value.num
                else:
                    raise TypeJsonParseError('checker', str(value) + ' is not number', value.line_col)
            elif name == 'Int':
                if isinstance(value, ast.Number) and not value.is_float:
                    value.res_value = value.num
                else:
                    raise TypeJsonParseError('checker', str(value) + ' is not int', value.line_col)
            elif name == 'Float':
                if isinstance(value, ast.Number):
                    value.res_value = float(value.num)
                else:
                    raise TypeJsonParseError('checker', str(value) + ' is not float', value.line_col)
            elif name == 'String':
                if isinstance(value, ast.String):
                    value.res_value = value.string
                else:
                    raise TypeJsonParseError('checker', str(value) + ' is not string', value.line_col)
            elif name == 'Bool':
                if isinstance(value, ast.Boolean):
                    value.res_value = value.boolean
                else:
                    raise TypeJsonParseError('checker', str(value) + ' is not bool', value.line_col)
            elif name == 'List':
                if isinstance(value, ast.Array):
                    Checker.check_elements(type_ref_symbol.type_ref_symbols[0], value.elements)
                    res_value = []
                    for elem in value.elements:
                        res_value.append(elem.value.res_value)
                    value.res_value = res_value
                else:
                    raise TypeJsonParseError('checker', str(value) + ' is not array', value.line_col)
            elif name == 'Set':
                if isinstance(value, ast.Set):
                    Checker.check_elements(type_ref_symbol.type_ref_symbols[0], value.elements)
                    res_value = []
                    for elem in value.elements:
                        res_value.append(elem.value.res_value)
                    value.res_value = res_value
                else:
                    raise TypeJsonParseError('checker', str(value) + ' is not set', value.line_col)
            else:
                raise Exception('bug! unknown built-in type ' + name)
        else:
            if isinstance(value, ast.Set) and type_specified:  # XX(a,b,c)
                # consider as constructor
                Checker.check_construct(type_ref_symbol, value)
            elif isinstance(value, ast.Object):
                Checker.check_object(type_ref_symbol, value)
            else:
                raise TypeJsonParseError('checker', str(value) + ' is not object', value.line_col)

    @staticmethod
    def check_elements(generic_type, elements):
        for ele in elements:
            Checker.check_is_instance(generic_type, ele)

    @staticmethod
    def check_construct(type_symbol, a_set):
        if len(a_set.elements) == 1:
            type_value = a_set.elements[0]
            if not type_value.type_symbol:
                Checker.check_value(type_symbol, False, type_value.value)
                a_set.res_value = a_set.elements[0].res_value
                return
            raise TypeJsonParseError('checker', str(a_set) + ' is not ' + str(type_symbol), a_set.line_col)
        else:
            a_def = type_symbol.a_def
            if Checker.__count_all_members(a_def) == len(a_set.elements):
                members = Checker.__list_all_non_generic_members(a_def).reverse()
                res_value = {}
                for i in range(0, len(members)):
                    mem = members[i]
                    ele = a_set.elements[i]
                    if isinstance(ele.value, ast.Null) and not mem.optional_type_symbol.optional:
                        raise TypeJsonParseError('checker', 'null is not allowed in member ' + mem.key.string,
                                                 ele.line_col)
                    Checker.check_is_instance(mem.optional_type_symbol.type_symbol, ele)
                    res_value[mem.key.string] = ele.value.res_value
                a_set.res_value = res_value
            else:
                raise TypeJsonParseError('checker', 'cannot construct ' + str(type_symbol), a_set.line_col)

    @staticmethod
    def check_object(type_ref_symbol, obj):
        def_members = Checker.list_all_members_of(type_ref_symbol)
        members = obj.members
        # other_type means the type of key "_"
        other_type = None
        key_types = {}
        # select the `other_type` and all key: type entries
        for def_mem in def_members:
            key = def_mem.key.string
            if key == '_':
                other_type = def_mem.optional_type_symbol
            else:
                key_types[key] = def_mem.optional_type_symbol
        # check each value's type
        for mem in members:
            key = mem.key.string
            type_value = mem.value
            if key in key_types:
                Checker.__check_value_optional_type(key, key_types[key], type_value)
            else:
                if other_type:
                    Checker.__check_value_optional_type(key, other_type, type_value)
                else:
                    raise TypeJsonParseError('checker',
                                             'key ' + str(key) + ' is not allowed in type ' + type_ref_symbol,
                                             obj.line_col)
            del key_types[key]
        # check whether keys left
        for key in key_types:
            optional_type = key_types[key]
            if optional_type.optional:
                continue
            raise TypeJsonParseError('checker', 'key ' + str(key) + ' is not optional', obj.line_col)
        # assign res_value
        res_value = {}
        for member in members:
            k = member.key.string
            v = member.value.value.res_value
            res_value[k] = v
        obj.res_value = res_value

    @staticmethod
    def __check_value_optional_type(key, optional_type_ref_symbol, type_value):
        optional = optional_type_ref_symbol.optional
        expected_type_ref_symbol = optional_type_ref_symbol.type_ref_symbol
        actual_type_ref_symbol = type_value.type_ref_symbol
        value = type_value.value

        if actual_type_ref_symbol:
            # type specified, check two types, then check value with given type
            Checker.check_assignable_from(expected_type_ref_symbol, actual_type_ref_symbol)
            Checker.check_value(actual_type_ref_symbol, True, value)
            res_value = Checker.__extract_res_value(type_value)
        else:
            # check value directly
            Checker.check_value(expected_type_ref_symbol, False, value)
            res_value = Checker.__extract_res_value(type_value)

        # check res_value exist if optional is False
        if not optional and res_value is None:
            raise TypeJsonParseError('checker', 'key ' + key + ' is not optional', type_value.line_col)

    @staticmethod
    def __extract_res_value(type_value):
        type_ref = type_value.type_ref_symbol
        value = type_value.value
        if (isinstance(value, ast.Set) and type_ref is None) or isinstance(value, ast.Array):
            res_value = []
            for e in value.elements:
                res_value.append(Checker.__extract_res_value(e))
            value.res_value = res_value
            return res_value
        elif isinstance(value, ast.Object):
            res_value = {}
            for member in value.members:
                k = member.key.string
                v = Checker.__extract_res_value(member.value)
                res_value[k] = v
            value.res_value = res_value
            return res_value
        else:
            return value.res_value

    @staticmethod
    def extract_result(type_obj):
        result_obj = {}
        obj_value = type_obj.value
        for member in obj_value.members:
            k = member.key.string
            v = member.value.value.res_value
            result_obj[k] = v
        return result_obj

    @staticmethod
    def check_assignable_from(expected_ref, actual_ref):
        if expected_ref.type_symbols:
            if actual_ref.type_symbols:
                e_gs = expected_ref.type_symbols
                a_gs = actual_ref.type_symbols
                if len(e_gs) != len(a_gs):
                    raise TypeJsonParseError('checker', 'generic types length mismatch', actual_ref.line_col)
                Checker.is_assignable_from_ignore_generic(expected_ref.type_tag.ast, actual_ref)
                for i in range(0, len(e_gs)):
                    e_g = e_gs[i]
                    a_g = a_gs[i]
                    Checker.is_assignable_from_ignore_generic(e_g.type_tag.ast, a_g)
            else:
                raise TypeJsonParseError('checker', 'missing generic types', actual_ref.line_col)
        else:
            if actual_ref.type_symbols:
                raise TypeJsonParseError('checker', 'unexpected generic types', actual_ref.line_col)
            else:
                Checker.is_assignable_from_ignore_generic(expected_ref.type_tag.ast, actual_ref)

    @staticmethod
    def __count_all_members(a_def):
        count = len(a_def.def_members)
        return count + Checker.__count_all_members(a_def.parent_type_ref_symbol.ast)

    @staticmethod
    def check_def_members(def_ast):
        tmp_map = {}
        def_members = def_ast.def_members
        for def_member in def_members:
            k = def_member.key.string
            if k in tmp_map:
                raise TypeJsonParseError('checker', 'duplicated key in definition ' + k, def_ast.line_col)
            optional_type_symbol = tmp_map[k] = def_member.optional_type_symbol
            Checker.check_generic_type(optional_type_symbol.type_ref_symbol)
        # check whether member is used in parents
        parent = def_ast
        while True and parent.parent_type_tag:
            parent = parent.parent_type_tag.ast.a_def
            for def_m in parent.def_members:
                k = def_m.key.string
                if k in tmp_map:
                    raise TypeJsonParseError('checker', 'duplicated key found in parent definition: ' + k,
                                             tmp_map[k].line_col)

    @staticmethod
    def check_generic_type(type_ref_symbol):
        if not type_ref_symbol.type_ref_symbols:
            return
        def_type = type_ref_symbol.type_tag.ast
        Checker.check_generics(type_ref_symbol.type_ref_symbols, def_type.generic_type_symbols,
                               type_ref_symbol.line_col)

    @staticmethod
    def check_is_instance(t, e):
        if e.type_ref_symbol:
            Checker.check_value(e.type_ref_symbol, True, e)
            Checker.check_ref_applies_def(e.type_ref_symbol, t)
        else:
            Checker.check_value(t, False, e.value)

    # p -> parent, s -> sub
    @staticmethod
    def is_assignable_from_ignore_generic(p, s):
        parent = s
        first = True
        while parent:
            if parent == p:
                return True
            if parent == s and not first:
                raise TypeJsonParseError('checker', 'circular inheritance detected', parent.line_col)
            parent = parent.a_def.parent_type_tag.ast
            first = False
        return False

    @staticmethod
    def check_ref_applies_def(r, d):
        if d.generic_type_symbols:
            if r.type_ref_symbols:
                # check generic len
                d_ls = d.generic_type_symbols
                r_ls = r.type_ref_symbols
                Checker.check_generics(r_ls, d_ls, r.line_col)
            else:
                # parent have generics, sub doesn't
                raise TypeJsonParseError('checker', "def type have generics, but ref doesn't", r.line_col)
        else:
            if r.type_ref_symbols:
                # parent doesn't have, sub does
                if d.built_in and d.type_name == 'Any':
                    return
                raise TypeJsonParseError('checker', "def type doesn't have generics, but ref does", r.line_col)
            else:
                # no generics
                return

    # ([type_ref_symbol]s on ref, [generic_type_symbol]s on def.type_symbol)
    @staticmethod
    def check_generics(type_args, type_params, line_col):
        if len(type_args) != len(type_params):
            raise TypeJsonParseError('checker', 'def and ref generic length mismatch', line_col)
        for i in range(0, len(type_args)):
            arg_ref_symbol = type_args[i]
            arg_symbol = arg_ref_symbol.type_tag.ast  # the def generic ast
            par_symbol = type_params[i].type_symbol_tag.ast  # parent type of the generic type
            if Checker.is_assignable_from_ignore_generic(par_symbol, arg_symbol):
                Checker.check_ref_applies_def(arg_ref_symbol, par_symbol)
            else:
                raise TypeJsonParseError('checker',
                                         "type " + str(arg_ref_symbol) + " cannot apply to " + str(par_symbol),
                                         arg_ref_symbol.line_col)
